Skip to content

feat(laurel): Add direct operational semantics for Laurel IR#511

Open
olivier-aws wants to merge 6 commits intostrata-org:mainfrom
olivier-aws:feat/laurel-direct-semantics
Open

feat(laurel): Add direct operational semantics for Laurel IR#511
olivier-aws wants to merge 6 commits intostrata-org:mainfrom
olivier-aws:feat/laurel-direct-semantics

Conversation

@olivier-aws
Copy link
Contributor

Summary

This PR implements Option A from the design document design-formal-semantics-for-laurel-ir.md: a standalone big-step operational semantics for all ~35 Laurel StmtExpr constructors, independent of Core semantics.

Changes

New Files

  1. Strata/Languages/Laurel/LaurelSemantics.lean (~800 LOC)

    • Core semantic definitions including LaurelValue, LaurelStore, LaurelHeap, Outcome types
    • Mutually inductive EvalLaurelStmt/EvalLaurelBlock relations
    • Coverage: literals, variables, primitive operations, control flow (if/while/block/exit/return), assignments, verification constructs (assert/assume), static calls, OO features (new/field select/instance call/reference equals/is type/as type), and specification constructs
  2. Strata/Languages/Laurel/LaurelSemanticsProps.lean (~500 LOC)

    • Store monotonicity theorems
    • Store operation determinism
    • catchExit and evalPrimOp properties
    • Block value semantics
    • Full evaluation determinism theorem (stated, admitted pending mutual induction proof)
  3. StrataTest/Languages/Laurel/LaurelSemanticsTest.lean (~300 LOC)

    • 30+ concrete evaluation tests covering all major constructs
    • Tests for literals, identifiers, primitive ops, blocks, if-then-else, exit/return propagation, local variables, assert/assume, nested control flow
  4. docs/designs/design-formal-semantics-for-laurel-ir.md

    • Design document describing three options (A: direct, B: shallow, C: hybrid)
    • This implementation is Option A, serving as a reference semantics for future translation correctness proofs

Key Design Decisions

  • Outcome type explicitly models non-local control flow (exit/return)
  • EvalArgs uses the expression evaluator δ (non-mutual) for call argument evaluation, avoiding Lean 4 mutual inductive limitations
  • Heap model is separate from store for OO features
  • Specification constructs (forall, exists, old, fresh, assigned, contract_of) are delegated to the expression evaluator δ
  • Static/instance calls restore the caller store after body evaluation

Bug Fixes (second commit)

  • Fixed EvalLaurelBlock non-determinism by adding rest ≠ [] side condition to cons_normal
  • Added void-returning procedure rules (static_call_return_void, instance_call_return, instance_call_return_void)
  • Fixed bindParams to enforce lexical scoping (start from empty store)
  • Added documentation for purity requirements and known limitations

Known Limitations

  • Determinism theorem is admitted (requires careful mutual induction)
  • While loop semantics only captures terminating executions (inductive)
  • Call argument evaluation uses δ rather than full EvalLaurelStmt
  • Translation correctness proof is out of scope (separate task)

Testing

All tests pass:

lake build

Related

Design document: docs/designs/design-formal-semantics-for-laurel-ir.md

Implement Option A from design-formal-semantics-for-laurel-ir.md: a
standalone big-step operational semantics for all ~35 Laurel StmtExpr
constructors, independent of Core semantics.

New files:
- Strata/Languages/Laurel/LaurelSemantics.lean: Core semantic definitions
  including LaurelValue, LaurelStore, LaurelHeap, Outcome types, and
  mutually inductive EvalLaurelStmt/EvalLaurelBlock relations covering
  literals, variables, primitive operations, control flow (if/while/
  block/exit/return), assignments, verification constructs (assert/
  assume), static calls, OO features (new/field select/instance call/
  reference equals/is type/as type), and specification constructs
  (forall/exists/old/fresh/assigned/prove by/contract of).

- Strata/Languages/Laurel/LaurelSemanticsProps.lean: Properties including
  store monotonicity (UpdateStore/InitStore preserve definedness),
  store operation determinism, catchExit properties, evalPrimOp
  determinism, and block value semantics. Full evaluation determinism
  theorem is stated but admitted (requires mutual induction).

- StrataTest/Languages/Laurel/LaurelSemanticsTest.lean: 30+ concrete
  evaluation tests covering literals, identifiers, primitive ops (add,
  and, not, lt, strconcat), blocks (empty, singleton, multi-statement),
  if-then-else (true/false/no-else), exit (propagation, labeled catch,
  non-matching), return (with/without value, short-circuit), local
  variables (init/uninit), assert/assume, prove-by, nested control
  flow, and evalPrimOp/catchExit unit tests.

- docs/designs/design-formal-semantics-for-laurel-ir.md: Design document
  describing three options (A: direct, B: shallow, C: hybrid) with
  recommendation for Option C. This implementation is Option A, serving
  as a reference semantics for translation correctness proofs.

Design decisions:
- Outcome type explicitly models non-local control flow (exit/return)
- EvalArgs uses the expression evaluator δ (non-mutual) for call
  argument evaluation, avoiding Lean 4 mutual inductive limitations
- Heap model is separate from store for OO features
- Specification constructs (forall, exists, old, fresh, assigned,
  contract_of) are delegated to the expression evaluator δ
- Static/instance calls restore the caller store after body evaluation

Known limitations:
- Determinism theorem is admitted (sorry) - requires careful mutual
  induction over EvalLaurelStmt/EvalLaurelBlock
- While loop semantics only captures terminating executions (inductive)
- Call argument evaluation uses δ rather than full EvalLaurelStmt
  (avoids mutual inductive nesting issues in Lean 4)
- Translation correctness proof is out of scope (separate task)
…mantics

Bug fixes:
- Fix EvalLaurelBlock non-determinism: add rest ≠ [] side condition to
  cons_normal so it cannot overlap with last_normal on singleton lists.
  last_normal handles [s] → value of s; cons_normal handles (s :: rest)
  where rest is non-empty.
- Add static_call_return_void, instance_call_return, and
  instance_call_return_void rules for void-returning procedures
  (.ret none). Previously these caused evaluation to get stuck.
- Fix bindParams to start from an empty store instead of the caller
  store, enforcing lexical scoping. Callees can no longer accidentally
  read caller locals.

Documentation:
- Document purity requirement for assert_true/assume_true conditions.
- Document intentionally omitted constructors (Abstract, All, Hole).
- Document known limitations (multi-target Assign, pure argument eval).
- Update design document: replace Option C recommendation with Option A
  decision record reflecting the actual implementation choice.
@olivier-aws olivier-aws requested a review from a team March 3, 2026 23:21
EvalLaurelStmt δ π h σ s h₁ σ₁ o₁ →
EvalLaurelStmt δ π h σ s h₂ σ₂ o₂ →
h₁ = h₂ ∧ σ₁ = σ₂ ∧ o₁ = o₂ := by
sorry
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we actually do the proof?

Copy link
Contributor Author

@olivier-aws olivier-aws Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, updated the PR (thanks Kiro for the proof)

EvalLaurelBlock δ π h σ ss h₁ σ₁ o₁ →
EvalLaurelBlock δ π h σ ss h₂ σ₂ o₂ →
h₁ = h₂ ∧ σ₁ = σ₂ ∧ o₁ = o₂ := by
sorry
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we actually do the proof?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, updated the PR (thanks Kiro for the proof)

@thanhnguyen-aws
Copy link
Contributor

I think we may need to define the two functions StmtExpr.SizeOf : StmtExpr -> Nat and StmtExprs.SizeOf : List StmtExpr -> Nat to prove the two theorems that @tautschnig mentioned above. Even then, I am not sure if we can prove that the size is decreasing as the Eval process is not guaranteed to be terminated.

Add HeapFieldWrite_deterministic and EvalArgs_deterministic lemmas to
LaurelSemanticsProps.lean as building blocks for the full
EvalLaurelStmt determinism proof.

HeapFieldWrite_deterministic: given the same heap, address, field, and
value, the resulting heap is unique. Proof by funext and by_cases on
address equality.

EvalArgs_deterministic: given the same evaluator, store, and expression
list, the resulting value list is unique. Proof by induction on the
first derivation, using functionality of the pure evaluator δ.

AllocHeap_deterministic is NOT included because AllocHeap is
non-deterministic in the allocated address — the constructor
existentially picks any addr where h addr = none, so two derivations
can choose different addresses. The full EvalLaurelStmt determinism
proof will need either a weaker formulation (heap bisimilarity up to
address renaming) or AllocHeap must be made deterministic (e.g., pick
the smallest free address).

Design ref: docs/designs/design-formal-semantics-for-laurel-ir.md
Chosen design: Option A (Direct Semantics) — auxiliary lemmas for
sub-relations to support the main determinism theorem.
…mmas for heap operations and EvalArgs

Remove false EvalLaurelStmt_deterministic / EvalLaurelBlock_deterministic
theorems that claimed full determinism including heap equality. The new_obj
rule uses AllocHeap which existentially picks any free address, making the
heap component non-deterministic. A sorry-admitted false theorem is worse
than no theorem because downstream proofs could silently inherit unsoundness.

Replace with a design-options comment documenting three approaches for
recovering a determinism result: deterministic allocator, bisimilarity up
to address renaming, or restricted heap-free determinism.

Add example-based tests exercising HeapFieldWrite_deterministic and
EvalArgs_deterministic on concrete derivations.
Prove full determinism for the Laurel big-step operational semantics:
given the same inputs, EvalLaurelStmt and EvalLaurelBlock produce
identical heaps, stores, and outcomes.

Design: docs/designs/design-formal-semantics-for-laurel-ir.md (Option A)
Approach: Mutual structural recursion on the first derivation (term-mode
match on H1, tactic-mode cases on H2 inside each branch).

Changes:
- LaurelSemantics.lean: Add smallest-free-address condition to AllocHeap
  (∀ a, a < addr → (h a).isSome) making allocation deterministic.
  This is design option 1 from the prior AllocHeap analysis.
- LaurelSemanticsProps.lean: Add AllocHeap_deterministic lemma and
  complete sorry-free proofs of EvalLaurelStmt_deterministic and
  EvalLaurelBlock_deterministic covering all constructor pairs.

Case structure:
- Trivial (13 leaf constructors): direct equality
- Single IH (8 cases): chain IH, subst, conclude
- Condition+branch (4 ite/while groups): IH on condition, cross-case
  contradiction via true≠false
- Outcome discrimination (while/call variants): IH gives outcome
  equality, discriminate normal vs exit vs return
- Multi-IH (field ops, assigns, calls): chain multiple IH applications

Known limitation: maxRecDepth 4096 and maxHeartbeats 800000 needed
due to the large number of mutual inductive constructors (~35 stmt,
5 block).
- Remove duplicate set_option directives before mutual block (bug #1)
- Add doc comment on AllocHeap.alloc noting bump-allocator invariant
  and implications for heap deallocation (design concern #1)
- Add TODO for heartbeat reduction via call-case helper lemma
  (design concern strata-org#2, recommendation strata-org#4)
- Remove unused catchExit_deterministic and evalPrimOp_deterministic
  (recommendation strata-org#3)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants